# -*- coding: utf-8 -*-
# created on 2016/05/17

# 求不含参数函数零点个数- 导数法 + 区间划分法 + Mathematica 求解法
from __future__ import division
from numpy import arange
from sympy import simplify, oo, Interval, limit
from sympy.functions.elementary.hyperbolic import HyperbolicFunction
from sympy.functions.elementary.trigonometric import TrigonometricFunction
from mathsolver.functions.base import BaseVariable, BaseFunction, BaseNumber, BaseFunc, BaseEq, BasePieceFunc, new_latex
from mathsolver.functions.hanshu.dingyiyu import QiuDingYiYu
from mathsolver.functions.hanshu.helper import check_func, check_inter, qujian_duandian
from mathsolver.functions.mathematica.mathematicaSolve import MathematicaSolve
from mathsolver.functions.sanjiao.sanjiao_utils import solve_trig_eq


def _remove_redundant_bounds(inters):
    """去掉重复的闭端点
    :param inters a list of Intervals"""
    assert all(isinstance(inter, Interval) for inter in inters)

    new_inters = []
    duandian_close = []
    for inter in inters:
        l_val, r_val, l_open, r_open = inter.args
        # 如果闭端点已经取到过，将闭端点改成开端点
        if (not l_open) and (l_val not in duandian_close):
            duandian_close.append(l_val)
        else:
            l_open = True

        if (not r_open) and (r_val not in duandian_close):
            duandian_close.append(r_val)
        else:
            r_open = True
        new_inters.append(Interval(l_val, r_val, l_open, r_open))
    return new_inters


def _process_dandiaoqujian(up, down, sort_interval=True):
    """单调区间预处理，输出是 [(-oo, -a], (-2, b], (b, oo)] 格式
    :param up 单调增区间 Interval/Union 类型
    :param down 单调减区间 Interval/Union 类型
    :param sort_interval 是否对单调区间排序
    :return a list of Intervals 合并起来恰好为定义域"""

    # 处理单调区间，合并为恰好定义域

    # 抽取出单个区间，并且按照区间大小从小到大排序
    inters = []
    for inter in (up, down):
        if isinstance(inter, Interval):
            inters.append(inter)
        else:
            inters.extend(inter.args)

    if sort_interval:
        inters.sort(key=lambda x: x.args[0])

    # 去掉重复端点
    inters = _remove_redundant_bounds(inters)

    return inters


class _LingDianGeShuBuHanCanSanJiao(BaseFunction):
    """求三角函数零点个数"""

    def solver(self, *args):
        func = check_func(args[0])
        expr, var = func.expression, func.var

        if len(args) > 1:
            inter = check_inter(args[1])
        else:
            inter = None

        # 补充：三角函数零点个数
        solve_set = solve_trig_eq(BaseEq([expr, '0']), str(var), inter)
        if solve_set:
            lingdian_geshu = len(solve_set)
        else:
            lingdian_geshu = 0

        self.steps.append(["", "函数 %s 一共有 %s 个零点" % (func.printing(), lingdian_geshu)])
        self.output.append(BaseNumber(lingdian_geshu))
        self.label.add("求函数零点个数")

        return self


class LingDianGeShuBuHanCanD(BaseFunction):
    """求不含参数函数零点个数- 导数法"""

    def solver(self, *args):

        func = check_func(args[0])
        expr, var = func.expression, func.var

        # 补充：三角函数零点个数
        if expr.has(TrigonometricFunction) or expr.has(HyperbolicFunction):
            return _LingDianGeShuBuHanCanSanJiao().solver(*args)

        # 计算单调区间
        from mathsolver.functions.hanshu.dandiaoqujian import DanDiaoQuJian
        dandiao_solver = DanDiaoQuJian().solver(func)
        self.steps.extend(dandiao_solver.steps)
        up, down = dandiao_solver.output[0].value

        # 预处理单调区间
        inters = _process_dandiaoqujian(up, down)

        lingdian_geshu = 0
        # 计算各个单调区间上的零点
        for inter in inters:
            left, right = inter.args[:2]
            f_left = limit(expr, var, left, "+")
            f_right = limit(expr, var, right, "-")

            try:
                # 乘积可能是 oo * 0
                f_left_multiply_right = f_left * f_right < 0
            except Exception:
                f_left_multiply_right = False

            if f_right == 0 or f_left_multiply_right:
                lingdian_geshu += 1
                self.steps.append(["", "函数在 %s 上有 1 个零点" % new_latex(inter)])
            else:
                self.steps.append(["", "函数在 %s 上无零点" % new_latex(inter)])

        self.steps.append(["", "综上所述函数 %s 一共有 %s 个零点" % (func.printing(), lingdian_geshu)])
        self.output.append(BaseNumber(lingdian_geshu))
        self.label.add("利用导数求函数零点个数")
        return self


class _LingDianBuHanCanFenDuanH(BaseFunction):
    """求不含参数分段函数零点个数 - 划分区间法

    输入 
        args[0] BasePiecefunc
        args[1] optional 区间
    """

    def solver(self, *args):
        func = args[0]
        expr, var = func.expression, func.var

        # 如果给定特定区间，只考虑给定区间
        has_qujian_arg = False
        if len(args) == 2:
            qujian = check_inter(args[1])
            has_qujian_arg = True

        self.steps.append(["", "考虑分段函数在每个分段上的零点个数"])
        lingdian_collector = 0
        for _expr, _range in expr:
            if has_qujian_arg:
                _range = _range.intersect(qujian)
            lingdian_geshu = LingDianGeShuBuHanCanH().solver(
                BaseFunc({'var': var, 'expression': _expr, 'type': '', 'name': 'f'}), _range).output[0].value

            lingdian_collector += lingdian_geshu
            self.steps.append(["", "在区间 %s 上，函数有 %s 个零点" % (new_latex(_range), lingdian_geshu)])

        self.steps.append(["", "综上所述，函数共有 %s 个零点" % lingdian_collector])
        self.output.append(BaseNumber(lingdian_collector))
        self.label.add("求分段函数零点个数")
        return self


class LingDianGeShuBuHanCanH(BaseFunction):
    """求不含参数函数零点个数 - 划分区间法

    输入 
        args[0] BaseFunc, BaseEq
        args[1] optional 区间
    """

    def solver(self, *args):
        func = check_func(args[0])
        # 补充： 分段函数零点个数
        if isinstance(func, BasePieceFunc):
            return _LingDianBuHanCanFenDuanH().solver(*args)

        expr, var = func.expression, func.var
        dingyiyu = QiuDingYiYu().solver(func)

        # 如果不给定特定区间、考虑区间 [-50, 50] 上面的特殊点
        range_x = range(-32, 32, 0.16)
        # 补充：如果给定特定区间，只考虑给定区间
        if len(args) == 2:
            qujian = check_inter(args[1])
            qujian_end = 32 if qujian.end == oo else qujian.end + 0.01
            qujian_start = -32 if qujian.start == -oo else qujian.start - 0.01
            range_x = arange(qujian_start, qujian_end, ((qujian_end - qujian_start) / 100).evalf())  # 取 100 个特殊点

        last = None
        num = 0
        for i in range_x:
            if i not in dingyiyu:
                continue
            if isinstance(expr, list):
                v = func.run(i)
            else:
                v = expr.subs({var: i})

            if "I" not in str(v):
                if last is not None:
                    try:
                        if last * v <= 0:
                            # print i
                            num += 1
                    except Exception:
                        continue
                last = v

        # 补充：如果定义域含有端点，检查端点是否是零点
        for dd in qujian_duandian(dingyiyu):
            if (dd not in [oo, -oo]) and (dd in dingyiyu) and (simplify(expr.subs(var, dd)) == 0):
                num += 1

        self.steps.append(["", "结合函数的图可知有 %s 个零点" % num])
        self.output.append(BaseNumber(num))
        self.label.add("求函数零点个数")
        return self


class LingDianGeShuBuHanCanM(BaseFunction):
    """求不含参函数零点个数，Mathematica 解

    问题：有的方程 Mathematica 解不出来"""

    def solver(self, *args):
        func = check_func(args[0])
        expr, var = func.expression, func.var

        if len(args) == 2:
            inter = check_inter(args[1])
            m_solver = MathematicaSolve().solver(BaseEq([expr, 0]), BaseVariable([var]), {var: inter}).output[0].value[
                var]
        else:
            m_solver = MathematicaSolve().solver(BaseEq([expr, 0]), BaseVariable([var])).output[0].value[var]

        if not m_solver:
            result = 0
        else:
            result = len(m_solver)

        self.output.append(BaseNumber(result))
        self.steps.append(["", "结合函数的图可知有 %s 个零点" % result])
        self.label.add("求函数零点个数")
        return self


class LingDianGeShuBuHanCan(BaseFunction):
    """求函数零点个数- 尝试导数法 + 划分区间法 + Mathematica 解法"""

    def solver(self, *args):
        clses = [LingDianGeShuBuHanCanD, LingDianGeShuBuHanCanH, LingDianGeShuBuHanCanM]

        for cls in clses:
            try:
                return cls().solver(*args)
            except Exception:
                continue

        raise RuntimeError("cannot solve problem")
